package org.example.mq.brokerserver.datacenter;

import org.example.mq.brokerserver.core.MQqueue;
import org.example.mq.brokerserver.core.Message;
import org.example.mq.common.BinaryTool;
import org.example.mq.common.MqException;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

//对硬盘上的信息进行管理
public class MessageManager {
    public static class Stat{
        //这里使用public，就不用get和set方法了。
        public int totalCount;
        public int validCount;
    }
    public void init(){
        //TODO
    }
    /*data下有队列目录，队列目录下有一个queue——data.txt表示消息内容，queue_stat.txt表示消息统计信息：
    * 两列，一列表示总消息数，一列表示有效消息数目。*/
    //获取队列所在的文件目录
    public String getQueueDir(String queueName){
        return "./data/"+queueName;
    }
    //获取消息的txt目录
    public String getMessageDir(String queueName){
        return getQueueDir(queueName)+"/queue_data.txt";
    }
    //获取消息统计信息目录
    public String getStatPath(String queueName){
        return getQueueDir(queueName)+"/queue_stat.txt";
    }
    //对queue_stat.txt信息进行读取
    private Stat readStat(String queueName) throws FileNotFoundException {
        Stat stat = new Stat();
        try(InputStream inputStream = new FileInputStream(getStatPath(queueName))){
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    //使用OutputStream来为Stat写入信息
    private void writeStat(Stat stat,String queueName){
        try(OutputStream outputStream = new FileOutputStream(getStatPath(queueName))){
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount+"\t"+stat.validCount);
            printWriter.flush();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    //创建对应的文件和目录
    public void createQueueFiles(String queueName) throws IOException {
        //创建队列文件
        File queueDir = new File(getQueueDir(queueName));
        if(!queueDir.exists()){
            boolean Ok = queueDir.mkdirs();
            if(!Ok){
                throw new IOException("创建队列目录失败！"+queueDir.getAbsolutePath());
            }
        }
        //创建消息内容文件
        File messageFile = new File(getMessageDir(queueName));
        if(!messageFile.exists()){
            boolean Ok = messageFile.createNewFile();
            if(!Ok){
                throw new IOException("创建消息内容文件失败！"+messageFile.getAbsolutePath());
            }
        }
        //创建统计信息文件
        File statFile = new File(getStatPath(queueName));
        if(!statFile.exists()){
            boolean Ok = statFile.createNewFile();
            if(!Ok){
                throw new IOException("创建统计信息文件失败！"+statFile.getAbsolutePath());
            }
        }
        //初始化生成的统计信息文件
        Stat stat = new Stat();
        stat.validCount=0;
        stat.totalCount=0;
        writeStat(stat,queueName);
    }
    //判断队列文件是否存在
    public Boolean checkFilesExits(String queueName){
        File queueDir = new File(getQueueDir(queueName));
        if(!queueDir.exists()){
            return false;
        }
        //对于消息内容不判断（初识为空）
        File statFile = new File(getStatPath(queueName));
        return statFile.exists();
    }
    //删除队列的目录和文件
    public void destroyQueueFiles(String queueName) throws IOException {
        //先把统计信息文件和消息内容文件删了
        File statFile = new File(getStatPath(queueName));
        boolean Ok1 = statFile.delete();
        if(!Ok1){
            throw new IOException("删除统计信息文件失败！"+statFile.getAbsolutePath());
        }
        File messageFile = new File(getMessageDir(queueName));
        boolean Ok2 = messageFile.delete();
        if(!Ok2){
            throw new IOException("删除信息内容文件失败！"+messageFile.getAbsolutePath());
        }
        //最后删除队列目录
        File queueFile = new File(getQueueDir(queueName));
        boolean Ok3 = queueFile.delete();
        if(!Ok3){
            throw new IOException("删除队列目录文件失败！"+queueFile.getAbsolutePath());
        }
    }
//序列化文件来实现CRUD
//发送信息到对应的队列的对应文件中
    public void sendMessage(MQqueue queue, Message message) throws MqException, IOException {
        //1先检查队列文件是否存在
        Boolean Ok = checkFilesExits(queue.getName());
        if(!Ok){
            throw new MqException("[MessageFileManager]  队列对应的文件不存在！queueName="+queue.getName());
        }
        //2把Message转化为二进制，进行序列化
        byte[] messageBinary = BinaryTool.toBytes(message);
        //当多个线程同时写文件时会有线程安全问题
        synchronized (queue){//不同的队列就不用加锁了
            //3先获取文件的长度，计算出新的Message从哪里开始
            File messageFile = new File(getMessageDir(queue.getName()));
            message.setOffsetBen(messageFile.length()+4);//4个字节表示新的文件长度
            //计算出添加了新文件后的长度
            message.setOffsetEnd(messageFile.length()+4+messageBinary.length);
            //4现在可以把新消息写到文件末尾了,这里要加上true，不然前面的文件就无了
            try(OutputStream outputStream = new FileOutputStream(messageFile,true)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    dataOutputStream.writeInt(messageBinary.length);//写入消息的长度
                    dataOutputStream.write(messageBinary);//写入消息内容
                }
            }
            //5更新消息统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount+=1;
            stat.validCount+=1;
            writeStat(stat,queue.getName());
        }
    }
    //删除消息的方法
    //采用逻辑删除，将isValid设置为0，不然会产生大量碎片空间。
    //1.先将二进制序列化为Message对象
    //2.将isValid设置为0.
    //3.将数据重新写入文件
    //4.更新统计信息
    public void deleteMessage(MQqueue queue,Message message) throws IOException,ClassNotFoundException{
        synchronized (queue){
            try(RandomAccessFile randomAccessFile = new RandomAccessFile(getMessageDir(queue.getName()),"rw")){//这里采用随机读取,rw表示读写模式
                //将要删除的信息的那段文件获取
                //System.out.println("Before seek, file pointer position: " + randomAccessFile.getFilePointer());
                byte[] bytes = new byte[(int)(message.getOffsetEnd()-message.getOffsetBen())];
                //System.out.println("Trying to read " + bytes.length + " bytes");
                randomAccessFile.seek(message.getOffsetBen());//seek表示光标，类似于Redis的光标
                //System.out.println("After seek, file pointer position: " + randomAccessFile.getFilePointer());
                int len = randomAccessFile.read(bytes);
                //System.out.println("实际读取长度"+len);
                //将read出来的数据转化为Message对象
                Message deleteMessage = (Message) BinaryTool.toObject(bytes);
                //设置isValid为0
                deleteMessage.setIsValid((byte) 0x0);
                //重新写入文件
                byte[] Rebytes = BinaryTool.toBytes(deleteMessage);
                //先将光标位置放到开头
                randomAccessFile.seek(message.getOffsetBen());
                randomAccessFile.write(Rebytes);
            }
            //更新统计信息
            Stat stat = readStat(queue.getName());
            //判断一下更安全
            if(stat.validCount>0)
                stat.validCount-=1;
            writeStat(stat,queue.getName());
        }
    }
    //负责将Message从硬盘加载到内存中（使用链表）
    //在程序启动时调用，不用考虑多线程。
    public LinkedList<Message> loadMessage(String queueName) throws IOException,MqException,ClassNotFoundException{
        LinkedList<Message> messages = new LinkedList<>();
        try(InputStream inputStream = new FileInputStream(getMessageDir(queueName))){
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)){
                long currentSeek = 0;
                while(true){
                    //读取Message
                    //先读取长度
                    int messageLen = dataInputStream.readInt();
                    byte[] bytes = new byte[messageLen];
                    int actLen = dataInputStream.read(bytes);
                    if(actLen!=messageLen){
                        throw new MqException("[MessageManager] 文件格式可能出错了！");
                    }
                    //再反序列化为对象
                    Message message = (Message) BinaryTool.toObject(bytes);
                    //判断一下消息是否有效
                    if (message.getIsValid()==0x0){
                        //无效信息只改变光标
                        currentSeek += (bytes.length+4);
                        continue;
                    }else{
                        currentSeek+=(bytes.length+4);
                        message.setOffsetBen(currentSeek+4);
                        message.setOffsetEnd(currentSeek+4+messageLen);
                        messages.add(message);
                    }
                }
            }catch (EOFException e){//因为while循环只有读到EoF才停，所以使用异常来当终止条件
                System.out.println("[MessageManager] 成功加载Message！");
            }
        }
        return messages;
    }
    //检查该队列的Message是否要进行GC（数字无特殊意义）
    public boolean checkGC(String queueName) throws FileNotFoundException {
        //通过判断总消息数和有效消息数来判断
        Stat stat = readStat(queueName);
        if(stat.totalCount>2000 && (double) stat.validCount / (double)stat.totalCount<0.5){
            return true;
        }
        return false;
    }
    private String getMessageNewPath(String queueName){
        return getQueueDir(queueName)+"/queue_data_new.txt";
    }
    //实现GC的逻辑
    //先将有效的文件写到一个新文件里
    //把旧文件删除
    //将新文件的名字换为旧文件名字
    //更新stat
    public void gc(MQqueue queue) throws MqException, IOException, ClassNotFoundException {//因为设计到写操作，所以要用queue对象来加锁
        synchronized (queue){
            //记录一下GC的时间
            long gcBen = System.currentTimeMillis();
            //创建一个新文件
            File newFile = new File(getMessageNewPath(queue.getName()));
            //对异常情况处理
            if(newFile.exists()){
                throw new MqException("[MessageManager] gc文件已经存在！");
            }
            boolean ok = newFile.createNewFile();
            if(!ok){
                throw new MqException("[MessageManager] gc时创建新文件时失败！");
            }
            //将旧文件中的有效信息读取出来
            LinkedList<Message> messages = loadMessage(queue.getName());
            //将有效信息写入到新文件中
            try(OutputStream outputStream = new FileOutputStream(newFile)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    for(Message message:messages){
                        byte[] bytes = BinaryTool.toBytes(message);
                        dataOutputStream.writeInt(bytes.length);
                        dataOutputStream.write(bytes);
                    }
                }
            }
            //删除旧文件
            File oldFile = new File(getMessageDir(queue.getName()));
            ok = oldFile.delete();
            if(!ok){
                throw  new MqException("[MessageManager] GC时删除旧文件失败！");
            }
            //把新文件重命名为旧文件
             ok = newFile.renameTo(oldFile);
            if(!ok){
                throw new MqException("[MessageManager] GC时重命名文件失败!");
            }
            //更新stat
            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(stat,queue.getName());
            //记录GC什么时候结束
            long gcEnd = System.currentTimeMillis();
            System.out.println("[MessageManager] GC完毕！用时："+(gcEnd-gcBen)+"ms");

        }
    }
}
